# 
# Copyright (c) 2002-2007 Eric Wallengren
# This file is part of the Continuous Automated Build and Integration 
# Environment (CABIE)
# 
# CABIE is distributed under the terms of the GNU General Public
# License version 2 or any later version.  See the file COPYING for copying 
# permission or http://www.gnu.org. 
#                                                                            
# THIS MATERIAL IS PROVIDED AS IS, WITH ABSOLUTELY NO WARRANTY EXPRESSED OR  
# IMPLIED, without even the implied warranty of MERCHANTABILITY or FITNESS 
# FOR A PARTICULAR PURPOSE.  ANY USE IS AT YOUR OWN RISK. 
#                                                                            
# Permission to modify the code and to distribute modified code is granted, 
# provided the above notices are retained, and a notice that the code was 
# modified is included with the above copyright notice. 
# 

#
# This package is specifically for running the buildserver and source monitor
# under unix operating systems including Cygwin32.
#

BEGIN{push @LIB, "lib";}

#
# Package declaration
#
package unixsys;

#
# Use Carp for error handling
#
use Carp;
use Sys::Hostname;
use Proc::Killfam;
use queries;

#
# Buildconf for configuration values
#
my $hostname = hostname();
$hostname    =~ s/\.[a-zA-Z0-9]+//g;
my $configname = $hostname;
$configname    =~ s/-//g;

require "$configname.pm";

my $qs = new queries;

#
# For future use
#
# use Proc::ProcessTable;

#
# Uncomment if you're using MySQL
#
use DBI;
DBI->trace(0);

my $config = new $configname;

#
# Global section, please do not edit!!!
# All these values will be set at startup.
#
my %fields = (
    sport         => '8091',
    mport         => '8092',
    port          => '8091',
    makemonitor   => '0',
    makedaemon    => '0',
    bAmMonitor    => '0',
    bAmServer     => '0',
    serverpid     => '-1',
    monitorpid    => '-1',
    volume        => 'NA',
    jobname       => 'NA',
    jobno         => '0',
    hostname      => 'NA',
    verbosefuncs  => '0',
    verboselogs   => '1',
);

#
# Read commandline args
#
sub validateargs {

    my $self = shift;

    my $hostname = shift;

    my %Opts = @_;

    my $arg;
    my $bMissing = 0;
    my $sport;
    my $mport;

    my @posixargs    = qw (m s d);
   
    foreach $arg (@posixargs) {

        if (!defined($Opts{$arg})) {
           if ($bMissing == 0) {
               print "Incomplete options: ";
               $bMissing = 1;
           }
           print " -$arg";
        }
    }

    if ($bMissing) {
        print " [-v -t]";
        print "\n";
        exit -1;

    }

    #
    # Look for -s (server port)
    #
    if ($Opts{s} !~ /^$/ ) {
        if ($Opts{s} !~ /^[0-9]+$/ ) {
            print "Invalid port number!\n";
            exit 1;
        } else {
            $self->sport($Opts{s});
        }
    }

    #
    # Look for -m (monitor port)
    #
    if ($Opts{m} !~ /^$/ ) {
        if ($Opts{m} !~ /^[0-9]+$/ ) {
            print "Invalid port number!\n";
            exit 1;
        } else {
            $self->makemonitor('1');
            $self->mport($Opts{m});
            $self->sport($Opts{s});
        }
    }

    #
    # Look for -d (create server daemon)
    #
    if ($Opts{d}) {
        $self->makedaemon('1');
    }

    if ($Opts{t}) {
        $self->verbosefuncs('1');
    }

    if ($Opts{v}) {
        $self->verboselogs('1');
    }


}

#
# Startup daemons...
#
sub startdaemons {

    my $self = shift;
    my $dir  = shift;

    my $Mtmpdir = $config->BTMP;
    my $Mnull   = $config->NULL;

    my $serverpid;
    my $monitorpid;

    #
    # If -d was specified on the command line, become a daemon...
    #
    if (defined($self->makedaemon) && !$self->bAmServer) {
        $serverpid = fork;
        $self->serverpid("$serverpid");
    }

    #
    # If there was a separate port defined for a monitor, create a new
    # monitor process...
    #
    if ($self->makemonitor && $self->serverpid) {
        $monitorpid = fork;
        $self->monitorpid("$monitorpid");
    }

    #
    # If this is the child process then setup some defaults
    #
    if ($self->monitorpid == 0) {
        if ($self->makemonitor) {
            $self->bAmMonitor('1');
            $self->port($self->mport);
        }
    }

    #
    # If this is the server child process set port...
    #
    if ($self->serverpid == 0) {
        $self->bAmServer('1');
        $self->port($self->sport);
    }

    #
    # If this is the parent process, then monitorpid should be > 0
    #
    if ($self->monitorpid > 0) {

        $self->_logpid("monitor", $self->monitorpid);

    }

    #
    # If this is the parent process, then serverpid should be > 0
    #
    if ($self->serverpid > 0) {

        $self->_logpid("server", $self->serverpid);

    }

    if ($self->makedaemon) {
        # Server reaches this
        if ($self->serverpid && $self->monitorpid) {
            exit;
        }
    }

    # Startup the server here using new_server from Msg.
    if ($self->serverpid == 0) {
        print "***$port starting server daemon***\n";
        open STDIN, "$Mnull" ||die "open $!";
        open STDOUT, ">$dir/logs/bsdaemon.log" || die "open $!";
        open STDERR, ">$dir/logs/bserror.log" || die "open $!";
    }

    if ($self->monitorpid == 0) {
        print "***$port starting monitor daemon***\n";
        open STDIN, "$Mnull" ||die "open $!";
        open STDOUT, ">$dir/logs/msdaemon.log" || die "open $!";
        open STDERR, ">$dir/logs/mserror.log" || die "open $!";
    }

}

#
# Fork a process and return child pid to calling app
#
sub forkprocess {

    my $self  = shift;
    my $cmd   = shift;
    my $wait  = shift;
    my $sleep = shift;

    my @args  = @_;

    my $pid;

    if ($pid = fork) {
        ;
    }
        elsif (defined $pid) { # $pid is zero here if defined
            sleep($sleep);
            exec($cmd, @args);
    }

    if ($wait) {

        my $sqlquery;
        my @sqlarray;

        #
        # Grab infor from config
        #
        my $title = $self->jobname;
        my $jobno = $self->jobno;
        my $host  = $self->hostname;

        #
        # Push pid onto process tree table
        #
        push @sqlarray, "$host";
        push @sqlarray, "$title";
        push @sqlarray, "$jobno";
        push @sqlarray, "$cmd @args";
        push @sqlarray, "$pid";

        $self->run_sql_submit("proctree", @sqlarray);

        #
        # Wait for process to complete
        #
        waitpid($pid, 0);

        #
        # Get the return status for the process
        #
        $status = $?/256;

        #
        # Remove all entries from proctree associated with this pid
        #
        $sqlquery = $qs->formatquery(107, $host, $title, $pid);

        $self->run_sql_remove("$sqlquery");

        #
        # Return status
        #
        return $status;

    } else {
        return $pid;
    }

}

#
# Use in place of system for prebuild, build, fail and 
# postbuild processes to track id's of running processes.
# Posix good, Win32 bad.
#
sub osprocesstree {

    my $self   = shift;
    my $server = shift;
    my $title  = shift;
    my $jobno  = shift;
    my $cmd    = shift;
    my @args   = @_;

    my $pid;
    my @parent;

    my @empty;
    my @sqlarray;
    my $sqlquery;
    my $recs;

    $sqlquery = $qs->formatquery(108, $server, $title, $jobno);

    @sqlarray = $self->run_sqlquery($sqlquery, ";");

    $recs = @sqlarray;

    @sqlarray = @empty;

    if ($pid = fork) {

        print STDERR "osprocesstree:\n\trec: $recs\n";
        
        if (!$recs) {

            @parent = self->getparentinfo($pid);

            my $ppid = $parent[0];
            my $pcmd = $parent[1];

            #
            # Push appropriate values into array
            #
            push @sqlarray, $server; 
            push @sqlarray, $title; 
            push @sqlarray, $jobno; 
            push @sqlarray, $pcmd; 
            push @sqlarray, $ppid; 
    
            print STDERR "osprocesstree:\n\t$host\t\n$title\n\t$jobno\n\t".
                         "(p)$pcmd\n\t(p)$ppid\n";
            #
            # Add active process to sql table
            #
            $self->run_sql_submit("proctree", @sqlarray);

            @sqlarray = @empty;

        }

        #
        # Push appropriate values into array
        #
        push @sqlarray, $server; 
        push @sqlarray, $title; 
        push @sqlarray, $jobno; 
        push @sqlarray, $cmd; 
        push @sqlarray, $pid; 

        #
        # Add active process to sql table
        #
        $self->run_sql_submit("proctree", @sqlarray);

        #
        # Wait for process to complete
        #
        print STDERR "osprocesstree:\n\tgoing to wait for $pid\n";
        waitpid($pid, 0);
        print STDERR "osprocesstree:\n\t$pid ended\n";
   
        $sqlquery = $qs->formatquery(109, $server, $title, $jobno, $pid);

        $self->run_sql_remove($sqlquery);

    }

    elsif (defined $pid) {
        print "executing $cmd @args\n";
        exec($cmd, @args);
    }

}

#
# get parent process info for supplied pid
#
sub getparentinfo {

    my $self = shift;
    my $pid  = shift;

    my $process;
    my $pstruct;

    my @return;
    
    $process = new Proc::ProcessTable;

    foreach $pstruct (@{$process->table}) {

        if ($pstruct->pid =~ /^$pid$/) {
            push @return, $pstruct->ppid;
            push @return, $pstruct->cmndline;
        }

    }

    return @return;
}

sub getloginname {

    my $self = shift;

    my $login = getlogin || getpwuid($<);

    return $login;

}

sub _logpid {

    my $self = shift;
    my $type = shift;
    my $pid  = shift;

    my $sqlquery;
    my @sqlarray;
    my @sqlsubmit;

    $sqlquery = $qs->formatquery(110, $hostname, $type);

    @sqlarray = $self->run_sql_query("$sqlquery", ",", 0);

    if (@sqlarray == 0) {
        push @sqlsubmit, "$hostname";
        push @sqlsubmit, "$type";
        push @sqlsubmit, "$pid";
        $self->run_sql_submit("serverids", @sqlsubmit);
    } else {
        $sqlquery = $qs->formatquery(111, $pid, $hostname, $type);
        @sqlarray  = $self->run_sql_query("$sqlquery", ";", 0);
    }

}


#
# Get average MAX duration for the job
#
sub _get_average {

    my $self   = shift;
    my $server = shift;
    my $title  = shift;
    my $total  = 0;
    my $t      = 0;
    my $keep;

    my $sqlquery;
    my @sqlarray;

    my $line;

    $sqlquery = $qs->formatquery(112, $title, $server);

    @sqlarray = $self->run_sql_query($sqlquery, ";", 0);

    $keep = $sqlarray[0];

    $sqlquery = $qs->formatquery(113, $title, $server, 2, $keep);

    @sqlarray = $self->run_sql_query($sqlquery, ";", 0);

    $max = 0;
    $min = 0;
    $sum = 0;
    $rec = @sqlarray;

    foreach $line (@sqlarray) {

        ($start, $end) = split(/;/, $line);
        $elapsed = $end-$start;

        if (!$min) {
            $min = $elapsed;
        }

        if ($elapsed < $min) {
            $min = $elapsed;
        } 

        if ($elapsed > $max) {
            $max = $elapsed;
        } 

        $sum += $elapsed;

    }

    if ($rec > 0) {
        if ($rec == 1) {
            $max *= .1;
            return $max;
        } 
        if ($rec == 2) {
            $total  = $sum/2;
            $t      = $total * .1;
            $total += $t;
            return $total;
        }
        if ($rec > 2) {
            $rec   = $rec - 2;
            $sum  -= $max;
            $sum  -= $min;
            $total = $sum / $rec;
            $t     = $total * .1;
            $total += $t;
            return $total;
        }
    
    } else {
        return 0;
    }

}

sub _get_hosttime {

    my $self = shift;

    my $sqlquery = "select unix_timestamp()";

    my @sqlarray = $self->run_sql_query($sqlquery, ";");

    return $sqlarray[0];

}

sub run_sql_submit {

    my $self   = shift;
    my $table  = shift;
    my @values = @_;

    my $picture;
    my $count  = 0;

    my $sqlserver = $config->SQLSERVER;
    my $sqlid     = $config->SQLID;
    my $sqlpass   = $config->SQLPASS;
    my $dbh;
    my $sth;

    my $ret = 1;

    $dbh = DBI->connect("dbi:mysql:database=builds;host=$sqlserver",
        "$sqlid", "$sqlpass"); 

    if (defined($dbh)) {

        while ($count < @values) {
            $picture .= "?, ";
            $count++;
        }

        $picture =~ s/, $//g;

        $sth = $dbh->prepare("INSERT INTO $table VALUES($picture)");

        $sth->execute(@values);

        $sth->finish();

        $dbh->disconnect;

    } else {
        $ret = 0;
    }

    return $ret;

}

sub run_sql_remove {

    my $self       = shift;
    my $sqlquery   = shift;

    my $sqlserver  = $config->SQLSERVER;
    my $sqlid      = $config->SQLID;
    my $sqlpass    = $config->SQLPASS;

    my $dbh;
    my $sth;

    my $ret        = 1;

    $dbh = DBI->connect("dbi:mysql:database=builds;host=$sqlserver",
        "$sqlid", "$sqlpass");

    if ($sqlquery =~ /\"\"/ && defined($dbh)) {
        $dbh->disconnect;
        undef $dbh;
    }

    if (defined($dbh)) {
        $sth = $dbh->prepare($sqlquery);
        $sth->execute();
        $sth->finish();
        $dbh->disconnect;
    } else {
        $ret = 0;
    }

    return $ret;

}

sub run_sql_query {

    my $self       = shift;
    my $sqlquery   = shift;
    my $delimeter  = shift;
    my $direction  = shift;

    my $sqlserver  = $config->SQLSERVER;
    my $sqlid      = $config->SQLID;
    my $sqlpass    = $config->SQLPASS;

    my $dbh;
    my $sth;
    my $line;
    my $record;

    my @ret;
    my @row;

    my $rv;

    if ($sqlquery =~ /delete from/) {
        return "cannot delete records from query function";
    }

    $dbh = DBI->connect("dbi:mysql:database=builds;host=$sqlserver",
        "$sqlid", "$sqlpass");

    if ($sqlquery =~ /\"\"/ && defined($dbh)) {
       $dbh->disconnect;
       undef $dbh;
    }

    if (defined($dbh)) {

        $sth = $dbh->prepare($sqlquery);

        $rv = $sth->execute();

        #
        # If this statement does not start with 'update'
        #
        if ($sqlquery !~ /^update/ic) {
            while ( @row = $sth->fetchrow_array) {
                $record = "";
                foreach $line (@row) {
                    chomp $line;
                    $record .= "$line"."$delimeter";
                }
    
                $record =~ s/$delimeter$//g;
    
                if ($direction) {
                    unshift @ret, $record;
                } else {
                    push @ret, $record;
                }
    
            }
        }

        $sth->finish();

        $dbh->disconnect;

        return @ret;

    } else {
        return undef;
    }

}

#
# Routine for finding free disk space...
#
sub diskspace {

    my $self      = shift;
    my $type      = shift;
    my $free;
    my $line;

    my $outfs     = $config->JOBDIR;
    my $promotion = $config->PROMOTION;
    my $df        = $config->DFCMD;

    my @diskinfo;
   
    if ($type) {
        @diskinfo  = `$df $promotion`;
    } else {
        @diskinfo  = `$df $outfs`;
    }

    foreach $line (@diskinfo) {
        chomp $line;
        if ($line =~ /\//) {
            $line =~ s/ +/ /g;
            my ($volume, $blks, $used, $nums) = split(/ /, $line);
            $self->volume($volume);
            $free = $nums * 1024;
        }
    }

    return $free;

}

#
# Routine for displaying system information
#
sub sysinfo {

    my $self = shift;

    my $info = `uname -a`;

    return $info;

}

#
# Write logs
#
sub tracefunction {

    my $self   = shift;
    my $string = shift;

    my $formattime;
    my $reqtime = scalar localtime;

    if ($fields{verbosefuncs}) {
        print STDERR "[$reqtime]: $string\n";
    }

}

#
# Kill recursively all processes
#
sub killall {

    my $self = shift;
    my $pid  = shift;

    print "Killing $pid and it's children\n";

    killfam 'TERM', ($pid);

}
sub writelog {

    my $self   = shift;
    my $string = shift;
    my $F      = shift;

    my $formattime;
    my $reqtime = scalar localtime;

    if ($fields{verboselogs}) {
        print $F "[$reqtime]: $string\n";
    }

}

#
# Do not edit anything below this line...
#
# Object constructor...
#
sub new {

    my $that = shift;
    my $class = ref($that) || $that;
    my $self = {
        %fields,
    };
    bless $self, $class;
    return $self;
}

#
# Autoload definitions in this package...
#
sub AUTOLOAD {

    my $self = shift;
    my $type = ref($self) || croak "$self is not an object";
    my $name = $AUTOLOAD;
    $name =~ s/.*://;
    unless (exists $self->{$name}) {
        croak "Can't access `$name` field in an object of class $type";
    }
    if (@_) {
        return $self->{$name} = shift;
    } else {
        return $self->{$name};
    }
}

1;
